firefly を使ってWebサービスを実装してみよう
fireflyについて
firefly はPythonの拡張ライブラリで、Function as a Serviceのためのフレームワークです。登録した関数をWebサービスとして展開することができます。 Webサービスを実装するためのフレームワークには多くのものが存在していますが、firefly は関数を登録するだけなので、おどろくほど簡単に実装することができます。
firefly は2018年12月からソースコードの更新が止まっています。これは設計がシンプルであるために新規機能の追加がなければ手を入れる理由がないからでしょう。知名度も高くないためか、firefly というキーワードだけで検索すると、Firefly というゲーム作成用のエンジンや、検索アルゴリズムの方が多くヒットしてしまいます。
それでも、firefly を一度利用してみればその有用性は理解できるはずです。
インストール
firefly のインストールは pip で行いま
code: bash\
$ pip install firefly-python
利用方法
Python で記述した次のような関数をもつ funcs.py があるとします。
code: funcs.py
def square(n):
return n**2
def cube(n):
return n**3
これを firefly で実行してみます。pythonスクリプト名.関数名を引数として与えます。
引数は複数与えることができます。
code: bash
$ firefly funcs.square
2021-07-11 07:37:54 firefly INFO Starting Firefly... curl を使ってこのWebサービスを利用してみましょう。
firefly が待ち受けているアドレス:ポートに関数名をURLとして与えます。
code: bash
100
ほら!とっても簡単でしょう?
Pythonコードからは次のように利用します。
code: fibo_client.py
import firefly
ans = client.square(n=10)
print(ans)
code: bash
$ python fibo_client.py
100
もう少し詳しく説明しましょう
firefly のコマンドはいくつかオプションを取ることができます。オプション --help で参照することができます。
code: bash
firefly --help
2021-07-15 07:46:37 firefly INFO Starting Firefly... positional arguments:
functions functions to serve
optional arguments:
-h, --help show this help message and exit
--version Prints the firefly version
-t TOKEN, --token TOKEN
token to authenticate the requests
-b ADDRESS, --bind ADDRESS
-c CONFIG_FILE, --config CONFIG_FILE
--allow-origins ALLOW_ORIGINS
Origins to allow for cross-origin resource sharing
このオプションを説明することからはじめましょう。
bindオプション
待ち受けるアドレスとポート番号を指示します。デフォルトは localhost:8000 (つまり、127.0.0.1:8000)です。
code: bash
firefly -b localhost:8001 funcs.square
2021-07-11 07:52:46 firefly INFO Starting Firefly... アドレスは firefly を実行するサーバが持っているホスト名かIPアドレスを与えます。それ以外のIPアドレス(ホスト名)を与えるとエラーになります。
code: bash
$ firefly -b 198.13.33.164:8080 funcs.square
2021-07-11 07:56:51 firefly INFO Starting Firefly... Traceback (most recent call last):
File "/Users/goichiiisaka/anaconda3/envs/py39/bin/firefly", line 8, in <module>
sys.exit(main())
(中略)
File "/Users/goichiiisaka/anaconda3/envs/py39/lib/python3.9/socketserver.py", line 466, in server_bind
self.socket.bind(self.server_address)
OSError: Errno 49 Can't assign requested address tokenオプションで認証を行う
オプション--tokenに与えた文字列を使って認証を行わせることができます。
code: bash
firefly --token PythonOsaka funcs.square
2021-07-11 08:00:32 firefly INFO Starting Firefly... code: bash
100
code: bash
{
"error": "Invalid auth token"
}
code: bas
import firefly
ans = client.square(n=10)
print(ans)
code: bash
python fibo_client_auth.py
100
このときにパスワードで利用する文字列は、そのままネットワーク上に流れるので注意しましょう。
allow-originオプション
allow-origin オプションの理解にはWeb関連の知識が必要になりますが、簡単にいうとfirefly が提供するWebサービスへのアクセス方法を制限するためのものです。このオプションはデフォルトは無効になっていて、bindオプションで与えたURLでしかアクセスできないようになっています。
もう少しだけ詳しく説明すると、Webサービスにアクセスするために使われるURLのスキーム(プロトコル、ホスト(ドメイン)、ポートで構成される)が同じであるとき、同じオリジンであると言います。違うオリジンでのアクセスを許可したいといにこのオプションを使用します。
Webアプリケーションは、そのアプリケーションが読み込まれた同じオリジンに対してだけリソースをのリクエストを許可することができます。
fireflyが読み取る環境変数
firefly は引数の他にも環境変数を読み取ります。
FIREFLY_FUNCTIONS:実行する関数を"モジュール名.関数名"の形式で与えます。
FIREFLY_TOKEN:オプション--token で与えるトークン文字列
FIREFLY_ALLOW_ORIGINS:
FIREFLY_CONFIG:構成ファイルへのパス
configオプションで構成ファイルを与える
firefly の起動時に毎回オプションを与えるのは楽しくありませんし。あらかじめ定義した構成ファイルを--configオプションで与えることができます。
code: funcs.py
def square(n):
return n**2
def cube(n):
return n**3
code: funcs_config.yml
version: 1.0
token: "PythonOsaka"
functions:
square:
path: "/square"
function: "funcs.square"
cube:
path: "/cube"
function: "funcs.cube"
code: bash
$ firefly --config funcs_config.yml
2021-07-1! 08:22:29 firefly INFO Starting Firefly... code: bash
100
1000
code: bash
import firefly
ans1 = client.square(n=10)
ans2 = client.cube(n=10)
print(ans1)
print(ans2)
code: bash
$ python funcs_client_auth.py
100
1000
gunicornを使ってfirefly を起動する
Web アプリケーションでは、受信するHTTPリクエストを並列処理する方が、一度に 1 つのリクエストしか処理しない場合よりもサーバのリソースをより効率的に使用することができます。
gunicorn はWSGI アプリケーション用の純粋な Python HTTP サーバーで、FlaskやDjangoといったWebフレームワークでも連携して運用されることが多くあります。
code: bash
$ pip install gunicorn
まずは簡単にコマンドラインで、firefly を gunicorn でサービスさせてみましょう。
可読性をよくするためにバックスラッシュ記号で区切って改行してコマンドを入力しています。
code: bash
(service1) $ gunicorn --workers 3 \
--preload firefly.main:app \
--env FIREFLY_FUNCTIONS="funcs.square,funcs.cube" \
--env FIREFLY_TOKEN="PythoOsaka"
2021-07-11 08:44:40 firefly INFO Starting Firefly... gunicorn のオプション --workersに起動するワーカーの数を与えます。この例の場合3つのワーカープロセスが生成されます。
オプション--preloadでfirefly モジュールの mainにあるappオブジェクトが読み込まれます。appオブジェクトは環境変数 FIREFLY_FUNCTIONS と FIREFLY_TOKEN を読み取ってfireflyのサービスが’起動します。
gunicorn をsystemd で起動する
systemd はサービスなどを提供するデーモンプロセスを起動/停止などの管理を行うためのツールで、Linuxでは一般的に使用されています。
この今回のfirefly のサービスを systemd に登録してみましょう。
サービス用のユーザを作成
まずユーザ webapp を作成して、ユーザ変更をしておきます。
この意図は一般ユーザとは異なるユーザを作っておくということです。
毎回サービスごとにユーザを作ると必要はありません
ユーザ名が webapp に限定されているわけではありません
同様の目的で作成したユーザを利用することもできます
サービスを立ち上げるサーバで sudo を利用してroot権限を取得できるユーザで、以下のスクリプトを実行して webapp ユーザを作成’します。
code: Create_User.sh
PW=$( python3 -c "import crypt; \
import getpass as gw; \
pw1=gw.getpass('NewUser Password:'); \
pw2=gw.getpass('NewUser Password Again:'); \
pw1 == pw2 and print(crypt.crypt(pw1, 'webapp'))" )
sudo useradd -m -d /var/webapp -p ${PW} -s /bin/bash webapp
grep -qs sudo /etc/group || sudo groupadd sudo
sudo bash -lc 'echo "%sudo ALL=(ALL) ALL" > /etc/sudoers.d/sudo"
sudo usermod -a -G sudo
code: bash
$ bash Create_User.sh
NewUser Password: xxxxx # 作成するユーザに設定するパスワード文字列
NewUser Password Again: xxxxx # 作成するユーザに設定するパスワード確認用
We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:
#1) Respect the privacy of others. #2) Think before you type. #3) With great power comes great responsibility. sudo password for iisaka: yyyyy $ su - webapp
password: xxxxx # 作成したユーザのパスワード
サービスで利用するPythoを独立させるために venv モジュールを使ってPyton環境 service1 を作成して、そこに firefly と gunicorn をインストールします。
code: bash
$ python -m venv /var/webapp/envs/funcs_service
$ source /var/webapp/envs/funcs_service/bin/activate
(funcs_service) $ cd $VIRTUAL_ENV
(funcs_service) $ pip install firefly-python gunicorn
これを行う目的はサービス運用の安定化です。Webサービスが’複数あるようなときで、同じPythonを利用していると将来モジュールの競合で問題になることがあります。それは、Webサービスが育ってゆき機能追加がされるようなときに追加したモジュールが別のWebサービスに影響を与えかねないためです。それぞれのWebサービスは独立したPython環境を使用するようにしておくと、こうした問題を回避することができます。
サービス起動スクリプトの作成
次に、gunicon を使って firefly のサービスの起動スクリプトを用意します。
APP_HOSTでホスト名もしくはIPアドレスを与えます。
code: /var/webapp/envs/funcs_service/RUN_APP.sh
APP_HOST=127.0.0.1
APP_PORT=8080
BASEDIR=$( dirname $0 )
BASEDIR=$( cd ${BASEDIR}; pwd )
source ${BASEDIR}/bin/activate
GUNICORN=$( which gunicorn )
exec ${GUNICORN} firefly.main:app -b ${APP_HOST}:${APP_PORT} \
--env FIREFLY_CONFIG="${BASEDIR}/funcs_config.yml" \
--config ${BASEDIR}/gunicorn_config.py
gunicorn_config.py は次のようなものです。
code: /var/webapp/envs/funcs_service/gunicorn_config.py
from multiprocessing import cpu_count
reload = True
preload_app = True
# Worker Processes
worker_class = 'sync'
workers = int(cpu_count()/2)
if workers <= 2:
workers = 1
elif workers > 8:
workers = 8
# Logging
logfile = '/tmp/gunicorn.log'
loglevel = 'info'
logconfig = None
# Socket
socket_path = 'unix://tmp/gunicorn.socket'
bind = socket_path
サービスの実態となる funcs.py です。
code: /var/webapp/envs/funcs_service/funcs.py
def square(n):
return n**2
def cube(n):
return n**3
firefly の構成ファイルです。
code: /var/webapp/envs/funcs_service/funcs_config.yml
version: 1.0
token: "PythonOsaka"
functions:
square:
path: "/square"
function: "funcs.square"
cube:
path: "/cube"
function: "funcs.cube"
ここで、RUN_APP.sh でサービスが起動することを確認しておきます。
code: bash
$ chmod 755 RUN_APP.sh
$ ./RUN_APP.sh
systemd への登録
うまく起動したら、今度は systemd のサービスとして登録しましょう。
code: funcs.service
Description=funcs service
After=network.target
PIDFile=/run/gunicorn/pid
User=webapp
Group=webapp
RuntimeDirectory=gunicorn
WorkingDirectory=/var/webapp/envs/funcs_service
ExecStart=/var/webapp/envs/funcs_service/RUN_APP.sh
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
PrivateTmp=false
WantedBy=multi-user.target
サービスを有効にして起動します。
code: bash
$ sudo systemctl enable $VIRTUAL_ENV/funcs.service
Created symlink from /etc/systemd/system/multi-user.target.wants/funcs.service to /var/webapp/envs/func_service/funcs.service.
Created symlink from /etc/systemd/system/funcs.service to /var/webapp/envs/func_service/funcs.service.
これで funcs.service のシンボリックリンクが登録されました。
もしこのファイルを修正するような場合は、systemd に再度読み込ませる必要があります。
code: bash
$ sudo systemctl daemon-reload
サービスの起動は次にように行います。
$ sudo systemctl start funcs.service
起動状況は status を使います。
code: bash
$ sudo systemctl status -l funcs.service
● funcs.service - funcs service
Loaded: loaded (/var/webapp/envs/func_service/funcs.service; enabled; vendor preset: disabled)
Active: active (running) since Thu 2021-07-11 04:53:28 UTC; 1s ago
Main PID: 5279 (RUN_APP.sh)
CGroup: /system.slice/funcs.service
├─5279 /bin/bash /var/webapp/envs/func_service/RUN_APP.sh
├─5290 /var/webapp/envs/func_service/bin/python /var/webapp/envs/func_service/bin/gunicorn firefly.main:app --bind 127.0.0.1:8000 --config /var/webapp/envs/func_service/gunicorn_config.py --env FIREFLY_CONFIG=/var/webapp/envs/func_service/funcs_config.yml
└─5292 /var/webapp/envs/func_service/bin/python /var/webapp/envs/func_service/bin/gunicorn firefly.main:app --bind 127.0.0.1:8000 --config /var/webapp/envs/func_service/gunicorn_config.py --env FIREFLY_CONFIG=/var/webapp/envs/func_service/funcs_config.yml
Jul 15 04:53:28 dev01 systemd1: Started funcs service. Jul 15 04:53:28 dev01 RUN_APP.sh5279: 2021-07-11 04:53:28 firefly INFO Starting Firefly... Jul 15 04:53:28 dev01 RUN_APP.sh5279: 2021-07-11 04:53:28 firefly INFO loading config file: /var/webapp/envs/func_service/funcs_config.yml サービスの停止は stop 、再起動は restart です。
code: bash
$ sudo systemctl stop funcs.service
$ sudo systemctl restart funcs.serivce
supervisorでWebサービスを管理させる
systemd に登録するためには root権限が必要になります。昨今では比較的安価にクラウドでサーバを利用することもできるため、実際には困ることは少ないでしょうが、企業や大学などの共用利用サーバを使っているような場合はroot権限を使えないことがほとんどです。
こうした場合は supervisor を使ってユーザレベルでサービスのプロセス管理を行わせることができます。
code: bash
$ python -m venv $HOME/envs/funcs_service
$ source $HOME/envs/funcs_service/bin/activate
(funcs_service) $ cd $VIRTUAL_ENV
(funcs_service) $ pip install firefly-python gunicorn supervisor
一般ユーザ で RUN_APP.sh が起動できるところまでの手順は、ディレクトリパスが頃なる程度でほぼ同じです。
先の例でもあった RUN_APP.sh でもできるだけディレクトリパスをハードコーディングしていないため、
そのまま利用できます。
code: /home/iisaka/envs/funcs_service/RUN_APP.sh
APP_HOST=127.0.0.1
APP_PORT=8000
BASEDIR=$( dirname $0 )
BASEDIR=$( cd ${BASEDIR} ; pwd )
source ${BASEDIR}/bin/activate
GUNICORN=$( which gunicorn )
exec ${GUNICORN} firefly.main:app -b ${APP_HOST}:${APP_PORT} \
--env FIREFLY_CONFIG="${BASEDIR}/funcs_config.yml" \
--config ${BASEDIR}/gunicorn_config.py
funcs.py と funcs_config.yml、および gunicorn_config.py もそのままで利用できます。
次のスクリプトを実行すると supervisor と funcs_service のプロセス管理のための設定ができます。
code: Setup_Supervisord.sh
source $HOME/envs/funcs_service/bin/activate
mkdir -p ${VIRTUAL_ENV}/log/supervisor
mkdir -p ${VIRTUAL_ENV}/etc/{supervisor,supervisord.conf.d}
echo_supervisord_conf > ${VIRTUAL_ENV}/etc/supervisord.conf
sed -e "s|\
logfile=/tmp/supervisord.log|\
logfile=${VIRTUAL_ENV}/log/supervisord.log|" \
-e "s|\
pidfile=/tmp/supervisord.pid|\
pidfile=${VIRTUAL_ENV}/etc/supervisor/supervisord.pid|" \
-e "s|\
serverurl=unix:///tmp/supervisor.sock|\
serverurl=unix:///${VIRTUAL_ENV}/etc/supervisor/supervisor.sock|" \
-e "s|\
file=/tmp/supervisor.sock|\
file=${VIRTUAL_ENV}/etc/supervisor/supervisor.sock|" \
-e "s|\
-e "s|\
;files = relative/directory/\*\.ini|\
files = supervisord.conf.d/*.conf|" \
-i ${VIRTUAL_ENV}/etc/supervisord.conf
# Additional Service
cat <<_EOF_ > ${VIRTUAL_ENV}/etc/supervisord.conf.d/funcs_service.conf
directory=${VIRTUAL_ENV}
command=${VIRTUAL_ENV}/RUN_APP.sh
autostart=true
autorestart=true
user=${USER}
stdout_logfile=${VIRTUAL_ENV}/log/supervisor/funcs_service.log
redirect_stderr=true
_EOF_
supervisord を起動します。
code: bash
$ supervisord
素っ気なく終わったように見えますが、バックグランドで動作しています。
code: bas
(funcs_service) $ supervisorctl status
funcs_service RUNNING pid 26424, uptime 0:00:21
管理下においいたプロセスの制御は supervisorctl コマンドで行います。
基本的な操作は start, stop, restart がほとんどです。
code: bash
(funcs_service) $ supervisorctl stop funcs_service
funcs_service: stopped
(funcs_service) $ supervisorctl status
funcs_service STOPPED Jul 16 12:52 AM
(funcs_service) $ supervisorctl start funcs_service
funcs_service: started
(funcs_service) $ supervisorctl status
funcs_service RUNNING pid 26725, uptime 0:00:03
supervisord.conf や funcs_server.conf の構成ファイルを修正したときは reload を行う必要があります。
code: bash
$ supervisorctl help
default commands (type help <topic>):
=====================================
add exit open reload restart start tail
avail fg pid remove shutdown status update
clear maintail quit reread signal stop version
SSHなどネットワーク経由でログインしているような場合でも、セッションを切ってしまっても、 supervisord は動作し続けます。
supervisord 自身は軽量で停止することはありませんが、サーバが停止したときは当然止まってしまいますし、サーバが起動したときに自動起動されるわけではないことに留意してください。
まとめ
firely を使うとPythonの関数を簡単にWebサービスとして提供することができるようになります。エンタープライズレベルでのWebサービスとしてはフレームワークは機能不足ですし、REST APIに準拠しているわけではありません。
しかし、それでもネットワークを経由してリクエストを処理できるようになることは便利ですし、応用範囲も広いものです。
学習コストも低いので試す価値はあるでしょう。
参考